敏感数据脱敏
日志、接口返回里经常要把手机号、身份证、邮箱等脱敏后再输出。配合 Jackson 的自定义序列化器,加一个注解就能声明「这个字段序列化时脱敏」,POJO 本身不动。
整体三件套:脱敏类型枚举 + 注解 + 序列化器。
脱敏类型
用正则描述每种脱敏规则:
public enum SensitiveType {
/** 姓名:只保留首字,后面加 ** */
NAME(Pattern.compile("(\\S)\\S+"), "$1**"),
/** 手机号:保留前三位和后四位 */
MOBILE(Pattern.compile("(\\d{3})\\d{4}(\\d{4})"), "$1****$2"),
/** 邮箱:保留首位和域名 */
EMAIL(Pattern.compile("(^.{1})[^@]*(@.*$)"), "$1****$2"),
/** 身份证:保留前六位和后四位 */
ID_CARD(Pattern.compile("(\\d{6})\\d+(\\d{4})"), "$1****$2");
private final Pattern pattern;
private final String replacement;
SensitiveType(Pattern pattern, String replacement) {
this.pattern = pattern;
this.replacement = replacement;
}
public String mask(String target) {
if (target == null || target.isEmpty()) {
return target;
}
return pattern.matcher(target).replaceAll(replacement);
}
}
地址类规则特殊(要按省市区截断),给对应枚举项覆写 mask 即可,这里略。
注解
借助 @JacksonAnnotationsInside 把 @JsonSerialize(using = ...) 打包进自定义注解,使用方只需 @Sensitive:
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerializer.class)
public @interface Sensitive {
/** 指定脱敏类型 */
SensitiveType value();
}
序列化器
难点在于:序列化器是单例 / 会被 Jackson 缓存,但每个字段的脱敏类型不同。解决办法是实现 ContextualSerializer,在 createContextual 里根据字段上的注解生成一个携带具体类型的新实例,而不是改写自身状态,这样才能线程安全。
public class SensitiveSerializer extends JsonSerializer<String> implements ContextualSerializer {
private final SensitiveType type;
/** Jackson 实例化时用 */
public SensitiveSerializer() {
this.type = null;
}
/** createContextual 传递状态用 */
public SensitiveSerializer(SensitiveType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (type == null) {
gen.writeString(value);
return;
}
gen.writeString(type.mask(value));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
if (property == null) {
return prov.findNullValueSerializer(null);
}
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (annotation != null) {
// 返回新实例,不修改 this.type,保证线程安全
return new SensitiveSerializer(annotation.value());
}
// 没有注解,回退到默认的 String 序列化器
return prov.findValueSerializer(property.getType(), property);
}
}
使用
@Setter
@Getter
public class User {
private String name;
@Sensitive(SensitiveType.MOBILE)
private String mobile;
@Sensitive(SensitiveType.EMAIL)
private String email;
@Sensitive(SensitiveType.ID_CARD)
private String idCard;
}
User user = new User("张三", "13812345678", "test@example.com", "110101199001011234");
System.out.println(objectMapper.writeValueAsString(user));
输出:
{
"name" : "张三",
"mobile" : "138****5678",
"email" : "t****@example.com",
"idCard" : "110101****1234"
}
POJO 里存的是原始值,仅在序列化时脱敏,反序列化、业务逻辑不受影响。